package tcln.sors.client.core.mvc;

import java.util.Map;

import tcln.commons.assertion.Assert;
import tcln.commons.beans.BeanUtils;
import tcln.commons.collections.CollectionUtils;
import tcln.commons.logging.Logger;
import tcln.sors.client.core.mvc.modelmapping.ModelDataMappingFactory;

/**
 * Abstract implementation for an {@link ApplicationController} which defines steps to get a concrete controller for a
 * request.
 * <p>
 * The work-flow to get a correct controller for a given request is:
 * <ol>
 * <li>Method {@link #lookupControllerClass(String)} is called to find out which controller class is suitable for given
 * controller type.
 * <li>Then, if a controller class is found, an instance of this controller is retrieved via
 * {@link #getControllerInstance(Class)}. This method, by default, creates a new instance of given controller class.
 * Subclasses may override this behaviour.
 * <li>After that, if possible, a {@link View} is looked up and bind to received/created controller instance. Two
 * methods for this purpose are {@link #lookupViewClass(String)} and {@link #getViewInstance(Class)}.
 * <li>Next, method {@link Controller#initialize(Map)} is invoked on received controller instance to initialize this
 * controller for this new context. Notice that this method is only called ONCE.
 * <li>Later, method {@link Controller#onReady()} provides the latest chance to customize data before showing.
 * <li>At this time, method {@link Controller#getCurrentModel()} should be available. Thus, this model is set back to
 * the corresponding view for display by invoking {@link View#setModel(ModelData)}.
 * <li>Finally, this controller is returned.
 * </ol>
 * 
 * @author Truong Chau Lien Ngoc (lienngoc@gmail.com)
 */
public abstract class AbstractApplicationController implements ApplicationController {
    protected Logger logger = Logger.getFor(getClass());

    private Map<String, Controller<?>> singletonControllerMap = CollectionUtils.newHashMap();
    private Map<String, View<?>> singletonViewMap = CollectionUtils.newHashMap();

    /**
     * {@inheritDoc}
     */
    @Override
    public Controller<?> getFor(StandardControllerType type, Map<String, Object> parameters) {
        Assert.ARGUMENTS.notNull(type, "Controller type is missing");
        return getFor(type.getValue(), parameters);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Controller<?> getFor(String type, Map<String, Object> parameters) {
        Controller<?> controller = null;

        Class<?> controllerClass = lookupControllerClass(type);
        if (controllerClass != null) {
            boolean controllerInitialized = true;

            // get/create controller
            if (isSingletonController(type)) {
                if ((controller = singletonControllerMap.get(type)) == null) {
                    controller = getControllerInstance(controllerClass);
                    controllerInitialized = false; // trigger controller initialization process
                    singletonControllerMap.put(type, controller);
                    logger.debug("Controller for type \"{0}\" is created. Instance: {1}", type, controller);
                } else {
                    logger.debug("Controller for type \"{0}\" is reused.", type);
                }
            } else {
                controller = getControllerInstance(controllerClass);
                controllerInitialized = false; // trigger controller initialization process
                logger.debug("Controller for type \"{0}\" is created. Instance: {1}", type, controller);
            }

            // get/create and set view to this controller is possible
            View<?> view = lookupView(type, parameters);
            if (view != null) {
                controller.setView(view);
            } else {
                Assert.STATE.fail("Cannot lookup view for controller type {0}", type);
            }

            // initialize controller
            if (!controllerInitialized) {
                controller.setModelDataMapper(ModelDataMappingFactory.getInstance().getMapper());
                controller.setModelDataPopulator(ModelDataMappingFactory.getInstance().getPopulator());
                controller.initialize(parameters);
            }

            // controller is ready to use
            controller.onReady();

            // afterwards, set model for display
            ((View) view).setModel(controller.getCurrentModel());
        }

        return controller;
    }

    private View<?> lookupView(String type, Map<String, Object> parameters) {

        // get/create and set view to this controller is possible
        Class<?> viewClass = lookupViewClass(type, parameters);
        if (viewClass != null) {

            View<?> view = null;
            if (isSingletonView(type)) {
                if ((view = singletonViewMap.get(type)) == null) {
                    view = getViewInstance(viewClass);
                    singletonViewMap.put(type, view);
                    logger.debug("View for type \"{0}\" is created. Instance: {1}", type, view);
                } else {
                    logger.debug("View for type \"{0}\" is reused.", type);
                }
            } else {
                view = getViewInstance(viewClass);
                logger.debug("View for type \"{0}\" is created. Instance: {1}", type, view);
            }

            return view;
        }

        return null;
    }

    /**
     * Checks if a given type requires a singleton controller.
     * 
     * @param type
     * @return
     */
    protected abstract boolean isSingletonController(String type);

    /**
     * Checks if a given type requires a singleton view.
     * 
     * @param type
     * @return
     */
    protected abstract boolean isSingletonView(String type);

    /**
     * Looks up a controller class which matches with given type.
     * 
     * @param type
     * @return A mapping class or <code>null</code> if given type is not yet supported.
     */
    protected abstract Class<?> lookupControllerClass(String type);

    /**
     * Looks up a view class which matches with given type.
     * 
     * @param type
     * @param parameters
     * @return A mapping class or <code>null</code> if given type is not yet supported.
     */
    protected abstract Class<?> lookupViewClass(String type, Map<String, Object> parameters);

    /**
     * Get/creates a new {@link Controller} instance base on its class.
     * 
     * @param controllerClass
     * @return
     */
    protected Controller getControllerInstance(Class<?> controllerClass) {
        Controller controller = (Controller) BeanUtils.newInstance(controllerClass);
        return controller;
    }

    /**
     * Get/creates a new {@link View} instance base on its class.
     * 
     * @param viewClass
     * @return
     */
    protected View<?> getViewInstance(Class<?> viewClass) {
        View<?> view = (View<?>) BeanUtils.newInstance(viewClass);
        return view;
    }
}
